using System;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security;
using System.Collections.Generic;
using System.Text;
using csammisrun.utility;

namespace csammisrun.MsnLib.Utility
{
  public delegate void NexusConnectedHandler(Nexus nexus);
  public delegate void NexusAuthenticatedHandler(Nexus nexus, string loginparameters);
  public delegate void NexusConnectionFailedHandler(Nexus nexus, string message);
  public delegate void NexusAuthenticationFailedHandler(Nexus nexus, string message);

  /// <summary>
  /// In the MSN lexicon, a nexus is the central point for user authentication
  /// </summary>
  public class Nexus
  {
    private Session _parent = null;
    private Dictionary<string, string> _challengedata =
      new Dictionary<string, string>();
    private SslStream _stream = null;

    private string _loginhost = "";
    private string _loginpath = "";

    private string _args = "";

    private const string HEAD_FORMAT = "GET {0} HTTP/1.1\r\nAuthorization: Passport1.4 OrgVerb=GET,OrgUrl={1},sign-in={2}";

    private const string TAIL_FORMAT = "lc={0},id={1},tw={2},fs={3},ru={4},ct={5},kpp={6},kv={7},ver={8},tpf={9}\r\n" +
                                       "User-Agent: MSMSGS\r\nHost: {10}\r\nConnection: Keep-Alive\r\nCache-Control: no-cache\r\n";

    public event NexusAuthenticatedHandler Authenticated;
    public event NexusAuthenticationFailedHandler AuthenticationFailed;
    public event NexusConnectionFailedHandler ConnectionFailed;

    /// <summary>
    /// Create a new nexus
    /// </summary>
    public Nexus(Session parent)
    {
      _parent = parent;
    }

    #region Nexus logic
    /// <summary>
    /// Connect the nexus to the authentication server
    /// </summary>
    public void Connect(string args)
    {
      _args = args;
      _parent.ProxiedSocketFactory("nexus.passport.com", 443, new ProxiedSocketFactoryResultHandler(EndNexusConnect));
    }

    private void EndNexusConnect(Socket socket, string errormsg)
    {
      if(!String.IsNullOrEmpty(errormsg))
      {
        string msg = "Could not connect to nexus.passport.com:" + Environment.NewLine + errormsg;
        if (this.ConnectionFailed != null)
        {
          this.ConnectionFailed(this, msg);
        }
        Disconnect();
        return;
      }

      TcpClient tc = new TcpClient();
      tc.Client = socket;
      _stream = new SslStream(tc.GetStream());
      _stream.BeginAuthenticateAsClient("nexus.passport.com", new AsyncCallback(EndNexusAuthenticate), null);
    }

    private void EndNexusAuthenticate(IAsyncResult res)
    {
      try
      {
        _stream.EndAuthenticateAsClient(res);
      }
      catch (Exception ex)
      {
        string msg = "Could not create SSL stream to nexus.passport.com: " + Environment.NewLine + ex.Message;
        if (this.ConnectionFailed != null)
        {
          this.ConnectionFailed(this, msg);
        }
        Disconnect();
        return;
      }

      _parent.OnLoginProgressUpdated(LoginStep.StepTwoConnectedToNexus);

      // The SSL stream is ready, send the necessaries to get the login server address
      byte[] request = Encoding.ASCII.GetBytes("GET /rdr/pprdr.asp\r\n\r\n");
      _stream.Write(request);
      byte[] reply = new byte[1024];
      _stream.BeginRead(reply, 0, reply.Length, new AsyncCallback(GotLoginServer), reply);
    }

    private void GotLoginServer(IAsyncResult res)
    {
      byte[] reply = null;
      int ppurls = -1;
      int dalogin = -1;
      int hostnameend = -1;
      int pathstart = -1;
      int pathend = -1;
      string response = "";

      try
      {
        int bytesread = _stream.EndRead(res);
        reply = (byte[])res.AsyncState;
        if (Marshal.IndexOf(reply, new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }, 0) == -1)
        {
          // If this happens twice, welcome to bone-ville
          _stream.BeginRead(reply, bytesread, reply.Length - bytesread, new AsyncCallback(GotLoginServer), reply);
        }
      }
      catch (Exception ex)
      {
        string message = "Couldn't read login server address:" + Environment.NewLine + ex.Message;
        if (this.ConnectionFailed != null)
        {
          this.ConnectionFailed(this, message);
        }
        Disconnect();
        return;
      }

      response = Encoding.ASCII.GetString(reply);
      if ((ppurls = response.IndexOf("PassportURLs")) == -1)
      {
        // Huh?
        Disconnect();
        return;
      }

      // DALogin=hostname[/path],
      if ((dalogin = response.IndexOf("DALogin=", ppurls)) != -1)
      {
        dalogin += "DALogin=".Length;
        hostnameend = response.IndexOfAny(new char[] { ',', '/' }, dalogin);
        _loginhost = response.Substring(dalogin, hostnameend - dalogin);
        pathstart = response.IndexOf('/', dalogin);
        if(pathstart == hostnameend)
        {
          pathend = response.IndexOf(',', pathstart);
          _loginpath = response.Substring(pathstart, pathend - pathstart);
        }
      }
      else
      {
        // Zuh?
        Disconnect();
        return;
      }

      _stream.Close();

      TcpClient tc = new TcpClient();
      tc.BeginConnect(_loginhost, 443, new AsyncCallback(EndLoginConnect), tc);
    }
    #endregion

    #region Login server logic
    private void EndLoginConnect(IAsyncResult res)
    {
      TcpClient tc = null;
      try
      {
        tc = res.AsyncState as TcpClient;
        tc.EndConnect(res);
      }
      catch (SocketException sockex)
      {
        string msg = "Could not connect to login server:" + Environment.NewLine + sockex.Message;
        if (this.ConnectionFailed != null)
        {
          this.ConnectionFailed(this, msg);
        }
        Disconnect();
        return;
      }

      _stream = new SslStream(tc.GetStream());
      _stream.BeginAuthenticateAsClient(_loginhost, new AsyncCallback(EndLoginAuthenticate), null);
    }

    private void EndLoginAuthenticate(IAsyncResult res)
    {
      try
      {
        _stream.EndAuthenticateAsClient(res);
      }
      catch (Exception ex)
      {
        string msg = "Could not create SSL stream to login server: " + Environment.NewLine + ex.Message;
        if (this.ConnectionFailed != null)
        {
          this.ConnectionFailed(this, msg);
        }
        Disconnect();
        return;
      }

      _parent.OnLoginProgressUpdated(LoginStep.StepThreeConnectedToLoginServer);

      // The SSL stream is ready, send the necessaries to get the login cookie
      GetLoginCookie();
    }

    private struct ReadProgress
    {
      public int BytesRead;
      public byte[] Buffer;
    }

    private void GetLoginCookie()
    {
      string head = String.Format(HEAD_FORMAT, _loginpath, GetChallengeData("ru"), HttpUtility.UrlEncode(_parent.Username));
      string request = head + ",pwd=" + HttpUtility.UrlEncode(_parent.Password) + "," + _args + "\r\n";
      request += "Host: " + _loginhost + "\r\n";
      request += "Connection: Keep-Alive\r\n\r\n";
      _stream.Write(Encoding.ASCII.GetBytes(request));

      ReadProgress rp = new ReadProgress();
      rp.BytesRead = 0;
      rp.Buffer = new byte[2048];
      _stream.BeginRead(rp.Buffer, rp.BytesRead, rp.Buffer.Length - rp.BytesRead, new AsyncCallback(GotCookie), rp);
    }

    private void GotCookie(IAsyncResult res)
    {
      ReadProgress rp = (ReadProgress)res.AsyncState;
      int index = -1;
      int end = -1;
      string response = "";
      string error = "";
      string parameters = "";

      try
      {
        rp.BytesRead += _stream.EndRead(res);
        if (Marshal.IndexOf(rp.Buffer, new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }, 0) == -1)
        {
          // If this happens twice, welcome to bone-ville
          _stream.BeginRead(rp.Buffer, rp.BytesRead, rp.Buffer.Length - rp.BytesRead, new AsyncCallback(GotCookie), rp);
        }
      }
      catch (Exception ex)
      {
        string message = "Couldn't read login cookie:" + Environment.NewLine + ex.Message;
        if (this.ConnectionFailed != null)
        {
          this.ConnectionFailed(this, message);
        }
        Disconnect();
        return;
      }

      response = Encoding.ASCII.GetString(rp.Buffer);
      // We're not going to support 302 Redirects at the moment, I am lazy
      if ((index = response.IndexOf("HTTP/1.1 401 Unauthorized")) != -1)
      {
        if ((index = response.IndexOf("WWW-Authenticate", index)) != -1)
        {
          if ((index = response.IndexOf("cbtxt=", index)) != -1)
          {
            index += "cbtxt=".Length;
            if ((end = response.IndexOfAny(new char[] { '\r', '\n' }, index)) != -1)
            {
              error = response.Substring(index, end - index);
            }
            else
            {
              error = response.Substring(index);
            }
          }
        }

        if (this.AuthenticationFailed != null)
        {
          this.AuthenticationFailed(this, HttpUtility.UrlDecode(error));
          Disconnect();
          return;
        }
      }
      else if ((index = response.IndexOf("HTTP/1.1 200 OK")) != -1)
      {
        index = response.IndexOf("Authentication-Info: ", index);
        if (index == -1)
        {
          if (this.AuthenticationFailed != null)
          {
            this.AuthenticationFailed(this, "Authenticated okay, but received no authentication information");
            Disconnect();
            return;
          }
        }

        index = response.IndexOf("from-PP='", index) + "from-PP='".Length;
        end = response.IndexOf("'", index);
        parameters = response.Substring(index, end - index);

        if(this.Authenticated != null)
        {
          this.Authenticated(this, parameters);
        }
      }
    }
    #endregion

    /// <summary>
    /// Disconnect from the nexus
    /// </summary>
    public void Disconnect()
    {
      if (_stream != null)
      {
        _stream.Close();
      }
    }

    /// <summary>
    /// Provide a default value for missing challenge data
    /// </summary>
    private string GetChallengeData(string key)
    {
      if (ChallengeData.ContainsKey(key))
      {
        return ChallengeData[key];
      }
      return "(null)";
    }

    #region Properties
    /// <summary>
    /// Gets the challenge data hashtable for user authentication
    /// </summary>
    public Dictionary<string, string> ChallengeData
    {
      get { return _challengedata; }
    }

    /// <summary>
    /// Gets or sets the parent session of this nexus
    /// </summary>
    public Session Parent
    {
      get { return _parent; }
      set { _parent = value; }
    }
    #endregion
  }
}
